Up
until recently, the iOS user interface paradigm supported showing only a
limited amount of material on the screen at any point in time. In a
Cocoa Touch application, there's typically one view controller in focus
at a time, and that view controller is in charge of the whole screen (or
most of it). The notable exceptions are classes like UINavigationController and UITabBarController, which don't display any interesting content on their own, but instead help developers organize other view controllers.
On the small screen of the
iPhone and the iPod touch, this makes a lot of sense. Instead of a
profusion of tiny widgets fighting for space on the screen, iOS users
have gotten used to being able to focus on one thing at a time, with new
views sliding into place when on-screen objects or controls are used.
This paradigm is so widely used that even controls that would take up
just a small space on a desktop computer, such as a popup list, fill the
iPhone's screen when you activate them. On the iPad, however, this
behavior isn't always suitable. Sometimes, you need to display a little
GUI in order to choose an option, such as from a popup list. Filling the
larger iPad screen with a simple list of items would feel both
unnatural and wasteful of that nice screen real estate!
The new UIPopoverController
class in iOS 3.2 lets you display an auxiliary view that floats in
front of the other on-screen content, without filling the entire screen.
Like the UINavigationController and UITabBarController, UIPopoverController
doesn't display any interesting content on its own. Instead, it serves
an organizational role and acts as a container for your own view
controllers.
1. Popover Preparations
So far, Dudel serves as a nice
demo of a few features, but it's extremely limited in comparison to the
vector-drawing applications that have been around for decades. One of
the main features it lacks is the ability to change the properties of
what you're drawing. Right now, you're stuck with the line width, stroke
and fill colors, and font that the app gives you from the outset. It's
time to change all that!
We're going
to create GUIs that let users change all those attributes, giving users
much more control over their creations. Each of these attributes
requires a little different approach to setting them, and therefore a
different sort of GUI:
Selecting a font will occur through a simple list that displays the name of each font, rendered in that font itself.
The font's size will be set using a slider in a popup, with a preview showing a piece of text rendered at the chosen size.
A popup with a slider will let you set the line width, again with a built-in preview.
Another popup will let you choose the fill and stroke colors from a predefined grid of colors.
The idea is for you to learn
several ways that popovers can be used in a real application, starting
with the simplest type and working up to more complicated examples.
Before we proceed, let's
clarify a point about the concept of a selected object, and the context
to which the attributes you set will be applied. Most vector-drawing
applications include some sort of selector tool that lets you click an
object you've drawn, which then becomes highlighted and editable in some
way. Any changes you make to color settings, line width, and so on are
typically applied immediately to the selected object. In Dudel, however,
we have none of that. There's never a selected object, and therefore
never any visible item to which your attribute settings are applied.
Instead, the settings are remembered in a central spot (the DudelViewController class), where they will be used for the next thing you draw.
1. The Basic GUI
Let's start off by making some modifications to the main GUI in DudelViewController. We're going add a set of new UIBarButtonItems
at the bottom of the screen for the popovers, each with a new icon.
Unlike the icons for the tools we created earlier, these don't need to
have any sort or highlighting state, so just a single icon for each is
fine. Table 1 shows the icons you'll need for this article. Add these images, using the filenames listed in Table 1, to your project.
Table 1. New Buttons for the Popovers
Filename | Image |
---|
button_strokewidth.png | |
button_strokecolor.png | |
button_fillcolor.png | |
button_fontname.png | |
button_fontsize.png | |
Now let's add action methods to our controller class's interface for connecting these buttons. Open DudelViewController.h, and somewhere near the end of the file, but before the @end line, add the following lines:
- (IBAction)popoverFontName:(id)sender;
- (IBAction)popoverFontSize:(id)sender;
- (IBAction)popoverStrokeWidth:(id)sender;
- (IBAction)popoverStrokeColor:(id)sender;
- (IBAction)popoverFillColor:(id)sender;
Then, just to keep our code in a compilable state, switch over to DudelViewController.m and insert some empty implementations for those methods inside the @implementation DudelViewController section:
- (IBAction)popoverFontName:(id)sender {
}
- (IBAction)popoverFontSize:(id)sender {
}
- (IBAction)popoverStrokeWidth:(id)sender {
}
- (IBAction)popoverStrokeColor:(id)sender {
}
- (IBAction)popoverFillColor:(id)sender {
}
We'll go back and fill in the
implementations of those methods a little later, but first we want to
hook up the GUI. Save your work, and then open DudelViewController.xib
in Interface Builder. We'll add the new buttons as a group, between the
group of tools on the left and the e-mail action on the right, as shown
in Figure 1.
First, duplicate the flexible space object in place, by selecting it and pressing =>D. That will give you a location to put more buttons. Then use the Library to find a UIBarButtonItem
and drag it out between the two flexible spaces. Next, open the
attribute inspector. Set the new item's Style to Plain, and set its
image to button_strokewidth.png. This gives us the basic template for how all five buttons will appear. While the new item is still selected, press =>D four times to make a row of five identical items.
Now we need to add the actions
and images to the buttons. Select the leftmost item, control-drag to the
File's Owner icon in the main .nib window, and click popoverStrokeWidth:
in the list of actions that appears. Then go along the rest of the row,
configuring each item's action and connecting it to the appropriate
image. The second item should get the popover_strokecolor.png image and be connected to popoverStrokeColor:. The third should use popover_fillcolor.png and popoverFillColor:.
The last two items are for choosing a font name and font size, and I'll
bet that by now, you can figure out which images and actions to use for
them.
2. Popover Considerations
One of the main uses for
popovers is to present a list of selectable items, not unlike the menus
available in Mac OS X and other desktop operating systems. When using
menus in a Mac OS X application, the system takes care of things such as
making sure that only one menu is shown at a time and making the menu
disappear when an item is selected. But the popover in iOS is a
different beast.
A popover won't automatically
disappear when the user selects something inside it, and opening one
popover doesn't remove any previously opened popover from the screen.
This means that you could easily wind up with multiple popovers on the
screen at once, overlapping each other.
The only time the system
automatically closes a popover is when you touch some part of the screen
outside the popover (except, notably, touching an item in a UIToolbar,
which leaves the popover just as it is). The rest of the time, you'll
need to dismiss the popover yourself any time a user action warrants it.
However, this apparent
lack of automation actually gives you some amount of flexibility
compared to what's typically possible with a menu. A popover can, for
instance, contain interactive controls, such as sliders or check boxes,
to let the user quickly try out different possibilities and see the
results instantly. That wouldn't be possible if the popover went away as
soon as someone clicked it. Similarly, allowing multiple popovers to be
displayed simultaneously may be useful in situations where you want to
let the user quickly change multiple settings or attributes. For
example, in a word processing app, you might want to let the user open
two popovers: one for selecting from a list of fonts, and one for
toggling attributes (bold, italics, underline, and so on).
NOTE
Apple
recommends against displaying multiple popovers at once, in order to
avoid "confusing" your users, so think twice before going that route.
In Dudel, we're going to allow for only one popover at a time by keeping an instance variable in DudelViewController that points at the current popover, and taking steps to make sure that it's properly managed. Start off by editing DudelViewController.h,
adding the following code shown in bold. In addition to adding the
instance variable (and its matching property declaration), here we're
also adding UIPopoverControllerDelegate to the list of protocols this class implements.
@interface DudelViewController : UIViewController <ToolDelegate, DudelViewDelegate, MFMailComposeViewControllerDelegate, UIPopoverControllerDelegate> {
id <Tool> currentTool;
IBOutlet DudelView *dudelView;
IBOutlet UIBarButtonItem *textButton;
IBOutlet UIBarButtonItem *freehandButton;
IBOutlet UIBarButtonItem *ellipseButton;
IBOutlet UIBarButtonItem *rectangleButton;
IBOutlet UIBarButtonItem *lineButton;
IBOutlet UIBarButtonItem *dotButton;
UIColor *strokeColor;
UIColor *fillColor;
UIFont *font;
CGFloat strokeWidth;
UIPopoverController *currentPopover;
}
@property (retain, nonatomic) id <Tool> currentTool;
@property (retain, nonatomic) UIColor *strokeColor;
@property (retain, nonatomic) UIColor *fillColor;
@property (retain, nonatomic) UIFont *font;
@property (assign, nonatomic) CGFloat strokeWidth;
@property (retain, nonatomic) UIPopoverController *currentPopover;
Follow up by switching over to DudelViewController.m to synthesize the currentPopover property, and clean it up in the dealloc method.
@synthesize currentTool, fillColor, strokeColor, font, strokeWidth, currentPopover;
- (void)dealloc {
self.currentTool = nil;
self.fillColor = nil;
self.strokeColor = nil;
self.currentPopover = nil;
[super dealloc];
}
One tricky aspect of dealing
with popovers has to do with cleanup after a popover has been dismissed.
If the user clicked outside the popover, causing it to be automatically
dismissed, then a method will be called in the UIPopoverController's delegate. But if you dismiss the popup from within code, that method isn't called. We'll handle this discrepancy by just making the delegate method call our own cleanup method, handleDismissedPopoverController:, which we'll be careful to call every time we dismiss a popup manually.
- (void)handleDismissedPopoverController:(UIPopoverController*)popoverController {
self.currentPopover = nil;
}
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController {
[self handleDismissedPopoverController:popoverController];
}
As you can see, our current cleanup method doesn't do much cleanup yet, but that will change!
The main thing we're going to need to do in our cleanup method, besides clearing our currentPopover
instance variable, is to get whatever values we need from the popover's
displayed controller. In Dudel, we'll implement this by checking for
the specific classes we're using for the view controllers. So the handleDismissedPopoverController: method will end up containing a series of if/else blocks, like this:
// just for explanatory purposes, not for copy-and-paste!
if ([popoverController.contentViewController isMemberOfClass:[SomeController class]]) {
// now we know which view controller we're dealing with
SomeController *sc = (SomeController *)popoverController.contentViewController;
// retrieve some values from the controller, to see what the user selected/adjusted
self.something = sc.something;
...
} else if (...)
Yes, I agree that this sort of if/else
pileup is distasteful. But it's the simplest solution in this case, and
our project is small enough that it's not introducing too much painful
ugliness.